package cn.cloud.all.security.web.util.matcher;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * Allows matching {@link HttpServletRequest} based upon the {@link MediaType}'s resolved
 * from a {@link ContentNegotiationStrategy}.
 * <p>
 * By default, the matching process will perform the following:
 *
 * <ul>
 * <li>The {@link ContentNegotiationStrategy} will resolve the {@link MediaType} 's for
 * the current request</li>
 * <li>Each matchingMediaTypes that was passed into the constructor will be compared
 * against the {@link MediaType} instances resolved from the
 * {@link ContentNegotiationStrategy}.</li>
 * <li>If one of the matchingMediaTypes is compatible with one of the resolved
 * {@link MediaType} returned from the {@link ContentNegotiationStrategy}, then it returns
 * true</li>
 * </ul>
 * <p>
 * For example, consider the following example
 *
 * <pre>
 * GET /
 * Accept: application/json
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * assert matcher.matches(request) == true // returns true
 * </pre>
 * <p>
 * The following will also return true
 *
 * <pre>
 * GET /
 * Accept: *&#47;*
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * assert matcher.matches(request) == true // returns true
 * </pre>
 *
 * <h3>Ignoring Media Types</h3>
 * <p>
 * Sometimes you may want to ignore certain types of media types. For example, you may
 * want to match on "application/json" but ignore "*&#47;" sent by a web browser.
 *
 * <pre>
 * GET /
 * Accept: *&#47;*
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
 * assert matcher.matches(request) == false // returns false
 * </pre>
 *
 * <pre>
 * GET /
 * Accept: application/json
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
 * assert matcher.matches(request) == true // returns true
 * </pre>
 *
 * <h3>Exact media type comparison</h3>
 * <p>
 * By default as long as the {@link MediaType} discovered by
 * {@link ContentNegotiationStrategy} returns true for
 * {@link MediaType#isCompatibleWith(MediaType)} on the matchingMediaTypes, the result of
 * the match is true. However, sometimes you may want to perform an exact match. This can
 * be done with the following examples:
 *
 * <pre>
 * GET /
 * Accept: application/json
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * matcher.setUseEquals(true);
 * assert matcher.matches(request) == true // returns true
 * </pre>
 *
 * <pre>
 * GET /
 * Accept: application/*
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * matcher.setUseEquals(true);
 * assert matcher.matches(request) == false // returns false
 * </pre>
 *
 * <pre>
 * GET /
 * Accept: *&#47;*
 *
 * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
 * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
 * matcher.setUseEquals(true);
 * assert matcher.matches(request) == false // returns false
 * </pre>
 *
 * @author Rob Winch
 * @author Dan Zheng
 * @since 3.2
 */

public final class MediaTypeRequestMatcher implements RequestMatcher {
    private final Log logger = LogFactory.getLog(getClass());
    private final ContentNegotiationStrategy contentNegotiationStrategy;
    private final Collection<MediaType> matchingMediaTypes;
    private boolean useEquals;
    private Set<MediaType> ignoredMediaTypes = Collections.emptySet();

    public MediaTypeRequestMatcher(MediaType... matchingMediaTypes) {
        this(new HeaderContentNegotiationStrategy(), Arrays.asList(matchingMediaTypes));
    }

    public MediaTypeRequestMatcher(Collection<MediaType> matchingMediaTypes) {
        this(new HeaderContentNegotiationStrategy(), matchingMediaTypes);
    }

    public MediaTypeRequestMatcher(ContentNegotiationStrategy contentNegotiationStrategy, MediaType... matchingMediaTypes) {
        this(contentNegotiationStrategy, Arrays.asList(matchingMediaTypes));
    }

    public MediaTypeRequestMatcher(ContentNegotiationStrategy contentNegotiationStrategy, Collection<MediaType> matchingMediaTypes) {
        Assert.notNull(contentNegotiationStrategy, "ContentNegotiationStrategy cannot be null");
        Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null or empty");
        this.contentNegotiationStrategy = contentNegotiationStrategy;
        this.matchingMediaTypes = matchingMediaTypes;
    }

    public boolean matches(HttpServletRequest request) {
        List<MediaType> httpRequestMediaTypes;

        try {
            httpRequestMediaTypes = this.contentNegotiationStrategy.resolveMediaTypes(new ServletWebRequest(request));
        } catch (HttpMediaTypeNotAcceptableException e) {
            this.logger.debug("Failed to parse MediaTypes, returning false", e);
            return false;
        }

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("httpRequestMediaTypes=" + httpRequestMediaTypes);
        }

        for (MediaType httpRequestMediaType : httpRequestMediaTypes) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Processing " + httpRequestMediaType);
            }
            if (shouldIgnore(httpRequestMediaType)) {
                this.logger.debug("Ignoring");
                continue;
            }
            if (this.useEquals) {
                boolean isEqualTo = this.matchingMediaTypes.contains(httpRequestMediaType);
                this.logger.debug("isEqualTo " + isEqualTo);
                return isEqualTo;
            }
            for (MediaType matchingMediaType : this.matchingMediaTypes) {
                boolean isCompatibleWith = matchingMediaType.isCompatibleWith(httpRequestMediaType);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug(matchingMediaType + " .isCompatibleWith " + httpRequestMediaType + " = " + isCompatibleWith);
                }
                if (isCompatibleWith) {
                    return true;
                }
            }
        }
        this.logger.debug("Did not match any media types");
        return false;
    }

    private boolean shouldIgnore(MediaType httpRequestMediaType) {
        for (MediaType ignoredMediaType : this.ignoredMediaTypes) {
            if (httpRequestMediaType.includes(ignoredMediaType)) {
                return true;
            }
        }
        return false;
    }

    public void setUseEquals(boolean useEquals) {
        this.useEquals = useEquals;
    }

    public void setIgnoredMediaTypes(Set<MediaType> ignoredMediaTypes) {
        this.ignoredMediaTypes = ignoredMediaTypes;
    }

    @Override
    public String toString() {
        return "MediaTypeRequestMatcher [contentNegotiationStrategy="
                + this.contentNegotiationStrategy + ", matchingMediaTypes="
                + this.matchingMediaTypes + ", useEquals=" + this.useEquals
                + ", ignoredMediaTypes=" + this.ignoredMediaTypes + "]";
    }
}